跳到主要内容

Spring 循环依赖问题(三级缓存)

Spring 是如何解决循环依赖的问题的。

何为循环依赖

就是这样:A、B、C 之间相互依赖

造成的问题

来一串代码说明问题

public class A {
private B b;
}

public class B {
private A a;
}

对应的 Bean 的配置

<bean id="beanA" class="xyz.coolblog.BeanA">
<property name="beanB" ref="beanB"/>
</bean>

<bean id="beanB" class="xyz.coolblog.BeanB">
<property name="beanA" ref="beanA"/>
</bean>

IOC 按照上面所示的 <bean> 配置,实例化 A 的时候发现 A 依赖于 B 于是去实例化 B(此时 A 创建未结束,处于创建中的状态),而发现 B 又依赖于 A ,于是就这样循环下去,最终导致 OOM

循环依赖发生的时机

Bean 实例化主要分为三步,如图:

问题出现在:第一步和第二步的过程中,也就是填充属性 / 方法的过程中(如果是使用构造方法注入就是第一步)

Spring 如何解决的(三级缓存)

Spring 为了解决单例的循环依赖问题,使用了 三级缓存 ,递归调用时发现 Bean 还在创建中即为循环依赖

单例模式的 Bean 保存在如下的数据结构中:

/** 一级缓存:用于存放完全初始化好的 bean **/
private final Map<String, Object> singletonObjects =
new ConcurrentHashMap<>(256);

/** 二级缓存:存放原始的 bean 对象(尚未填充属性),用于解决循环依赖 */
private final Map<String, Object> earlySingletonObjects =
new HashMap<>(16);

/** 三级级缓存:存放 bean 工厂对象,用于解决循环依赖 */
private final Map<String, ObjectFactory<?>> singletonFactories =
new HashMap<>(16);

bean 的获取过程:先从一级获取,失败再从二级、三级里面获取

创建中状态:是指对象已经 new 出来了但是所有的属性均为 null 等待被 init

检测循环依赖的过程如下:

1、A 创建过程中需要 B,于是 A 将自己放到三级缓里面 ,去实例化 B

2、B 实例化的时候发现需要 A,于是 B 先查一级缓存,没有,再查二级缓存,还是没有,再查三级缓存,找到了!

  • 然后把三级缓存里面的这个 A 放到二级缓存里面,并删除三级缓存里面的 A
  • B 顺利初始化完毕,将自己放到一级缓存里面(此时 B 里面的 A 依然是创建中状态)

3、然后回来接着创建 A,此时 B 已经创建结束,直接从一级缓存里面拿到 B ,然后完成创建,并将自己放到一级缓存里面

如此一来便解决了循环依赖的问题

// 以上叙述的源代码
protected Object getSingleton(String beanName, boolean allowEarlyReference) {
// 先从一级缓存找
Object singletonObject = this.singletonObjects.get(beanName);
// 设置当前这个 beanName 为创建状态
if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) {
synchronized (this.singletonObjects) {
// 再从二级缓存里面找
singletonObject = this.earlySingletonObjects.get(beanName);

if (singletonObject == null && allowEarlyReference) {
// 还找不到则使用三级缓存
ObjectFactory<?> singletonFactory = this.singletonFactories.get(beanName);

if (singletonFactory != null) {
singletonObject = singletonFactory.getObject();
// 把这个 beanName 存入二级缓存里面
this.earlySingletonObjects.put(beanName, singletonObject);
// 把当前这个 beanName 从三级缓存中删除
this.singletonFactories.remove(beanName);
}
}
}
}
return (singletonObject != NULL_OBJECT ? singletonObject : null);
}

二级缓存

我们现在已经知道,第三级缓存的目的是为了延迟代理对象的创建,因为如果没有依赖循环的话,那么就不需要为其提前创建代理,可以将它延迟到初始化完成之后再创建。

既然目的只是延迟的话,那么我们是不是可以不延迟创建,而是在实例化完成之后,就为其创建代理对象,这样我们就不需要第三级缓存了。因此,我们可以将 addSingletonFactory() 方法进行改造。

protected void addSingletonFactory(String beanName, ObjectFactory<?> singletonFactory) {
Assert.notNull(singletonFactory, "Singleton factory must not be null");

synchronized (this.singletonObjects) {
// 判断一级缓存中不存在此对象
if (!this.singletonObjects.containsKey(beanName)) {
// 直接从工厂中获取 Bean
Object o = singletonFactory.getObject();

// 添加至二级缓存中
this.earlySingletonObjects.put(beanName, o);
this.registeredSingletons.add(beanName);
}
}
}

这样的话,每次实例化完 Bean 之后就直接去创建代理对象,并添加到二级缓存中。

测试结果是完全正常的,Spring 的初始化时间应该也是不会有太大的影响,因为如果 Bean 本身不需要代理的话,是直接返回原始 Bean 的,并不需要走复杂的创建代理 Bean 的流程。

两级不能解决么?

测试证明,二级缓存也是可以解决循环依赖的。为什么 Spring 不选择二级缓存,而要额外多添加一层缓存呢?

但是解决循环依赖真的需要使用到三级缓冲吗?只使用两级缓存是否可以呢?

回顾一下三级缓存如何解决的?

如果 Spring 选择二级缓存来解决循环依赖的话,那么就意味着所有 Bean 都需要在实例化完成之后就立马为其创建代理,而 Spring 的设计原则是在 Bean 初始化完成之后才为其创建代理。(就是无法在初始化完对象后执行自定义的 init 方法了,具体看 Spring Bean 生命周期常见的拓展点 这篇笔记)

所以,Spring 选择了三级缓存。但是因为循环依赖的出现,导致了 Spring 不得不提前去创建代理,因为如果不提前创建代理对象,那么注入的就是原始对象,这样就会产生错误。(即无法初始化)

Reference

参考资料 一文说透 Spring 循环依赖问题 参考资料 高频面试题:Spring 如何解决循环依赖? 参考资料 Spring 解决循环依赖必须要三级缓存吗?